﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sinacor.OMM.HealthMonitor.Entity;
using Sinacor.OMM.Util;
using System.Net.Sockets;
using System.Net;
using Sinacor.OMM.HealthMonitor.Collector;
using System.Threading;
using Sinacor.OMM.Util;
using PSE.Framework.Threading;

namespace Sinacor.OMM.HealthMonitor.Client
{
    public class HealthMonitorClient : IDisposable
    {
        private const string TRACE_FOLDER_NAME = "Monitor Sistema";

        private const int TIME_TO_PROCESS_NEXT_MESSAGE_ON_STREAM = 1;

        private const int TIME_TO_WAIT_HEARTBEAT = 10000;
        private const int TIME_TO_SEND_HEARTBEAT = 5000;
        private const int TIME_TO_RETRY_CONNECTION = 10000;

        private static HealthMonitorClient _healthMonitorClient;
        private static Mutex _mutex = new Mutex();

        public delegate void HealthStatusHandler(HealthData healthData);
        public event HealthStatusHandler OnHealthStatusEvent;

        public delegate void HeartBeatHandler();
        public event HeartBeatHandler OnHeartBeatReceivedEvent;

        public delegate void ConnectHandler();
        public event ConnectHandler OnConnectEvent;

        public delegate void DisconnectHandler();
        public event DisconnectHandler OnDisconnectEvent;

        private HealthMonitorClientConfiguration _configuration = new HealthMonitorClientConfiguration();

        private Socket _sckClient;

        private TraceWriter _traceWriter;

        private bool _keepRunning;
        private bool _heartBeatReceived;
        private bool _connected;

        private bool _started;

        public static HealthMonitorClient GetInstance()
        {
            if (_mutex == null)
                _mutex = new Mutex();
            
            _mutex.WaitOne();

            if (_healthMonitorClient == null)
                _healthMonitorClient = new HealthMonitorClient();

            _mutex.ReleaseMutex();

            return _healthMonitorClient;
        }

        public static HealthMonitorClient GetInstance(int TcpServerPort, String TcpServerHostName)
        {
            if (_mutex == null)
                _mutex = new Mutex();

            _mutex.WaitOne();

            if (_healthMonitorClient == null)
                _healthMonitorClient = new HealthMonitorClient();

            _healthMonitorClient.Configuration.TcpServerHostName = TcpServerHostName;
            _healthMonitorClient.Configuration.TcpServerPort = TcpServerPort;

            _mutex.ReleaseMutex();

            return _healthMonitorClient;
        }

        private HealthMonitorClient()
        {
            _traceWriter = Tracer.GetInstance(TRACE_FOLDER_NAME, "HealthMonitorClient");
        }

        /// <summary>
        /// Configuração
        /// </summary>
        public HealthMonitorClientConfiguration Configuration
        {
            get { return _configuration; }
            set { _configuration = value; }
        }
          
        public void Start()
        {
            if (!_started)
            {
                _keepRunning = true;

                _traceWriter.Trace("Iniciou START()");
                
                Thread thrKeepConnectedOnServer = new Thread(new ThreadStart(KeepConnectedOnServer));
                thrKeepConnectedOnServer.Name = "Thread Keep Connected On Server";
                thrKeepConnectedOnServer.Start();

                Thread thrHeartBeatSender = new Thread(new ThreadStart(HeartBeatSender));
                thrHeartBeatSender.Name = "Thread HeartBeat Sender";
                thrHeartBeatSender.Start();

                Thread thrHeartBeatMonitor = new Thread(new ThreadStart(HeartBeatMonitor));
                thrHeartBeatMonitor.Name = "Thread HeartBeat Monitor";
                thrHeartBeatMonitor.Start();
            }
            else
            {
                _traceWriter.Trace("Já está em START(), não precisa iniciar tudo novamente...");
                if (_connected)
                {
                    _traceWriter.Trace("Emulou um novo evento OnConnect, por já estar conectado.");
                    DisparaEventoOnConnect();
                }
            }
            
            _traceWriter.Trace("Finalizou START()");
        }

        private void KeepConnectedOnServer()
        {
            _keepRunning = true;

            _traceWriter.Trace("Thread KeepConnectedOnServer Started...");

            while (_keepRunning)
            {
                _connected = false;

                try
                {
                    if(String.IsNullOrEmpty(Configuration.TcpServerHostName))
                        throw new ArgumentNullException("Tcp Server Host Name is null");

                    if (Configuration.TcpServerPort == 0)
                        throw new ArgumentNullException("Tcp Server Port is null");


                    int port = Configuration.TcpServerPort;
                    String host = Configuration.TcpServerHostName;

                    if (_sckClient == null)
                    {
                        _sckClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                    }

                    _traceWriter.Trace("Verificando conexão em [" + host + " : " + port + "] ...");
                    if (_sckClient != null && !_connected && !_sckClient.Connected)
                    {
                        try
                        {
                            _traceWriter.Trace("Tentará Conexão em [" + host + " : " + port + "] ...");
                            _sckClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                            _sckClient.Connect(host, port);
                            _connected = true;
                            _traceWriter.Trace("Conectou em [" + host + " : " + port + "] ...");

                            DisparaEventoOnConnect();

                            MessageReceiver();
                        }
                        catch (Exception ex)
                        {
                            _connected = false;
                            _traceWriter.Trace("Erro ao tentar se conectar em [" + host + " : " + port + "] ...");
                            _traceWriter.Trace(ex.StackTrace);
                        }
                    }
                    else
                    {
                        _traceWriter.Trace("Permanece conectado em [" + host + " : " + port + "] ...");
                    }
                }
                catch (Exception ex)
                {
                    _connected = false;
                    _traceWriter.Trace("Erro incomum...");
                    _traceWriter.Trace("Mensagem: " + ex.Message);
                    _traceWriter.Trace("StackTrace " + ex.StackTrace);
                }

                try
                {
                    CloseSocketClient();
                }
                catch (Exception ex)
                {
                    _traceWriter.Trace("Erro ao chamar CloseSocketClient...");
                }

                DisparaEventoOnDisconnect();

                _traceWriter.Trace("Aguardará [" + TIME_TO_RETRY_CONNECTION /1000 + "] segundos para tentar uma nova conexão ...");
                Sleeper.Sleep(TIME_TO_RETRY_CONNECTION, IsRunning);

                _sckClient = null;
            }

            _traceWriter.Trace("Thread KeepConnectedOnServer Stoped...");
        }

        private void DisparaEventoOnDisconnect()
        {
            try
            {
                if (OnDisconnectEvent != null)
                    OnDisconnectEvent();
            }
            catch (Exception ex)
            {
                _traceWriter.Trace("Erro ao disparar OnDisconnectEvent...");
                _traceWriter.Trace(ex.Message);
                _traceWriter.Trace(ex.StackTrace);
            }
        }

        private void DisparaEventoOnConnect()
        {
            try
            {
                if(OnConnectEvent != null)
                    OnConnectEvent();
            }
            catch (Exception ex)
            {
                _traceWriter.Trace("Erro ao disparar OnConnectEvent...");
                _traceWriter.Trace(ex.Message);
                _traceWriter.Trace(ex.StackTrace);
            }
        }

        private void MessageReceiver()
        {
            _traceWriter.Trace("Recebendo menssagens...");

            try
            {
                while (_sckClient != null && _connected && _sckClient.Connected && _keepRunning )
                {
                        HealthMonitorMessage msgRcvd = new HealthMonitorMessage();

                        try
                        {
                            _traceWriter.Trace("Receberá HEADER com [" + HealthMonitorMessage.HEADERSIZE + "] bytes", TraceType.Optional);
                            msgRcvd.header = ReceiveFromSocket(HealthMonitorMessage.HEADERSIZE);
                            _traceWriter.Trace("Recebeu HEADER [" + Encoding.Default.GetString(msgRcvd.header) + "] bytes", TraceType.Optional);


                            _traceWriter.Trace("Receberá MESSSAGETYPE com [" + HealthMonitorMessage.MESSAGETYPESIZE + "] bytes", TraceType.Optional);
                            msgRcvd.messageType = ReceiveFromSocket(HealthMonitorMessage.MESSAGETYPESIZE);
                            _traceWriter.Trace("Recebeu MESSSAGETYPE [" + Encoding.Default.GetString(msgRcvd.messageType) + "] bytes", TraceType.Optional);

                            int bodySize = Int32.Parse(Encoding.Default.GetString(msgRcvd.header));

                            _traceWriter.Trace("Receberá Mensagem com [" + bodySize + "] bytes", TraceType.Optional);
                            msgRcvd.body = ReceiveFromSocket(bodySize);
                            _traceWriter.Trace("Recebeu Mensagem com [\\r\\n" + Encoding.Default.GetString(msgRcvd.body) + "\\r\\n] fim da mensagem.", TraceType.Optional);

                            TrataMensagem(msgRcvd);

                            Sleeper.Sleep(TIME_TO_PROCESS_NEXT_MESSAGE_ON_STREAM, IsRunning);
                        }
                        catch (Exception ex)
                        {
                            _connected = false;
                            _traceWriter.Trace("Erro dentro da rotina que recebe mensagens (Comum no momento de destruir o objeto!");
                            _traceWriter.Trace("Passou o controle de estado para desconectado");
                            _traceWriter.Trace(ex.Message);
                        }
                        Sleeper.Sleep(TIME_TO_PROCESS_NEXT_MESSAGE_ON_STREAM, IsRunning);
                }
            }
            catch (Exception ex)
            {
                _traceWriter.Trace("ERRO :[" + ex.Message + "]");
                _traceWriter.Trace(ex.StackTrace);
            }

            _traceWriter.Trace("Parou de receber Mensagens...");
        }

        private void TrataMensagem(HealthMonitorMessage msgRcvd)
        {
            _traceWriter.Trace("Iniciou tratamento da mensagem...", TraceType.Optional);

            int intMsgType;
            intMsgType = Int16.Parse(Encoding.Default.GetString(msgRcvd.messageType));

            if (((int)HealthMonitorMessageType.HeartBeat) == intMsgType)
            {
                SetHeartBeatReceived();
                if (OnHeartBeatReceivedEvent != null)
                {
                    try
                    {
                        OnHeartBeatReceivedEvent();
                        _traceWriter.Trace("OnHeartBeatReceivedEvent disparado com sucesso:" ,TraceType.Optional);
                    }
                    catch (Exception ex)
                    {
                        _traceWriter.Trace("ERRO ao disparar OnHeartBeatReceivedEvent:[" + ex.Message + "]");
                        _traceWriter.Trace(ex.StackTrace);
                    }
                }
            }
            else if (((int)HealthMonitorMessageType.HealthStatus) == intMsgType)
            {
                try
                {
                    HealthData healthData = HealthData.Deserialize(msgRcvd.body);

                    if (OnHealthStatusEvent != null)
                    {
                        OnHealthStatusEvent(healthData);
                        _traceWriter.Trace("OnHeartBeatReceivedEvent disparado com sucesso:", TraceType.Optional);
                    }
                    else
                    {
                        _traceWriter.Trace("Ninguém assinou OnHeartBeatReceivedEvent", TraceType.Optional);
                    }
                }
                catch (Exception ex)
                {
                    _traceWriter.Trace("ERRO ao disparar OnHealthStatusEvent:[" + ex.Message + "]");
                    _traceWriter.Trace(ex.Message);
                    _traceWriter.Trace(ex.StackTrace);
                }
            }
        }

        public void HeartBeatSender()
        {
            _keepRunning = true;

            byte[] heartBeat = GetHeartBeat();

            _traceWriter.Trace("Thread HeartBeatSender Started...");

            while (_keepRunning)
            {
                try
                {
                    if (_sckClient != null)
                    {
                        if (_sckClient.Connected && _connected)
                        {
                            SendToSocket(heartBeat);
                            _traceWriter.Trace("Enviou Heart Beat para o Server!", TraceType.Optional);
                        }
                    }
                }
                catch (Exception ex)
                {
                    _connected = false;
                    _traceWriter.Trace("ERRO ao enviar HeartBeat...");
                }
                Sleeper.Sleep(TIME_TO_SEND_HEARTBEAT, IsRunning);
            }

            _traceWriter.Trace("Thread HeartBeatSender Stoped...");
        }

        private void SendToSocket(byte[] buffer)
        {
            if (_connected && _keepRunning)
            {
                _sckClient.Send(buffer, SocketFlags.None);
            }
        }

        private void HeartBeatMonitor()
        {
            int cont = 0;

            _traceWriter.Trace("Thread HeartBeatMonitor Started...");

            while (_keepRunning)
            {
                _heartBeatReceived = false;
                Sleeper.Sleep(TIME_TO_WAIT_HEARTBEAT, IsHeartBeatReceived);
                if (_heartBeatReceived)
                    cont = 0;
                else
                    cont++;

                if (cont >= 3)
                {
                    _traceWriter.Trace("Vai desconectar por não receber HeartBeat!");
                    this.CloseSocketClient();
                }
            }

            _traceWriter.Trace("Thread HeartBeatMonitor Stoped...");
        }

        private byte[] ReceiveFromSocket(int bytesQuantity)
        {
            int bytesReceived = 0;

            byte[] buffer = new byte[bytesQuantity];

            while (_sckClient.Connected && bytesReceived < bytesQuantity && _keepRunning)
            {
                bytesReceived = bytesReceived + _sckClient.Receive(buffer, SocketFlags.None);
            }

            return buffer;
        }

        private byte[] GetHeartBeat()
        {
            byte[] ret;

            ret = Encoding.UTF8.GetBytes(HealthMonitorMessage.HEARTBEATMESSAGE);

            return ret;
        }

        private void SetHeartBeatReceived()
        {
            _traceWriter.Trace("Recebeu Heart Beat do Server!", TraceType.Optional);
            _heartBeatReceived = true;
        }

        private bool IsHeartBeatReceived()
        {
            return !_heartBeatReceived && _keepRunning;
        }

        private bool IsRunning()
        {
            return _keepRunning;
        }

        private void CloseSocketClient()
        {
            _connected = false;

            if (_sckClient != null)
            {
                try
                {
                    if (_sckClient.Connected)
                    {
                        _sckClient.Shutdown(SocketShutdown.Both);
                        _sckClient.Disconnect(true);
                    }
                    _sckClient.Close();
                    _sckClient = null;
                }
                catch (Exception ex)
                {
                    _traceWriter.Trace("ERRO : [" + ex.Message + "]");
                    _traceWriter.Trace(ex.StackTrace);
                }
            }
        }

        public void Dispose()
        {
            _keepRunning = false;

            _traceWriter.Trace("Finalizando a conexão...");

            try
            {
                CloseSocketClient();
                _traceWriter.Trace("Conexão finalizada...");
            }
            catch (Exception ex)
            {
                _traceWriter.Trace("ERRO : [" + ex.Message + "]");
                _traceWriter.Trace(ex.StackTrace);
                _traceWriter.Trace("Saindo...");
            }

            _healthMonitorClient = null;

            if (_mutex != null)
            {
                _mutex.Close();
                _mutex = null;
            }

            _traceWriter.Trace("Connection Keeper Destroyed.");

        }


    }
}
